
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id: EntityModel.java 314 2010-02-10 18:52:16Z archie.cobbs $
 */

package org.dellroad.sidekar.schema.model;

import java.io.PrintStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

import org.dellroad.sidekar.Sidekar;
import org.dellroad.sidekar.schema.SchemaMismatchException;

/**
 * Schema type representing entity types.
 */
public class EntityModel extends AbstractModel {

    private final TreeMap<String, FieldModel> fieldModels = new TreeMap<String, FieldModel>();
    private final TreeMap<String, IndexModel> indexModels = new TreeMap<String, IndexModel>();

    private EntityModel parentModel;
    private boolean isAbstract;

    private Class<?> entityClass;

    // Normal constructor
    public EntityModel() {
    }

    // Copy constructor
    EntityModel(EntityModel original, SchemaModel schemaModel) {
        super(original);
        FieldDeepCopier fieldDeepCopier = new FieldDeepCopier(schemaModel);
        for (Map.Entry<String, FieldModel> entry : original.fieldModels.entrySet())
            this.fieldModels.put(entry.getKey(), entry.getValue().visit(fieldDeepCopier));
        for (Map.Entry<String, IndexModel> entry : original.indexModels.entrySet())
            this.indexModels.put(entry.getKey(), new IndexModel(entry.getValue()));
        if (original.parentModel != null)
            this.parentModel = schemaModel.getEntityModels().get(original.parentModel.getName());
        this.isAbstract = original.isAbstract;
        this.entityClass = original.entityClass;
    }

    /**
     * Get this entity's parent entity.
     *
     * @return this entity's parent entity, if any; otherwise {@code null}
     */
    public EntityModel getParentModel() {
        return this.parentModel;
    }
    public void setParentModel(EntityModel parentModel) {
        this.parentModel = parentModel;
    }

    /**
     * Get this entity's root ancestor entity.
     *
     * @return this entity's root ancestor entity, which will be this entity itself if this entity is a root entity
     */
    public EntityModel getRootEntityModel() {
        for (EntityModel ancestor = this; true; ancestor = ancestor.parentModel) {
            if (ancestor.parentModel == null)
                return ancestor;
        }
    }

    /**
     * Determine if this entity is the given entity or an ancestor of it.
     */
    public boolean isAncestorOf(EntityModel entityModel) {
        while (entityModel != null) {
            if (entityModel == this)
                return true;
            entityModel = entityModel.parentModel;
        }
        return false;
    }

    /**
     * Whether this entity is abstract.
     */
    public boolean isAbstract() {
        return this.isAbstract;
    }
    public void setAbstract(boolean isAbstract) {
        this.isAbstract = isAbstract;
    }

    /**
     * Get this entity's fields.
     *
     * @return fields mapped by name
     */
    public Map<String, FieldModel> getFieldModels() {
        return this.fieldModels;
    }

    /**
     * Get this entity's indexes.
     *
     * @return indexes mapped by name
     */
    public Map<String, IndexModel> getIndexModels() {
        return this.indexModels;
    }

    /**
     * Get this entity's user class.
     * This property is optional.
     */
    public Class<?> getEntityClass() {
        return this.entityClass;
    }
    public void setEntityClass(Class<?> entityClass) {
        this.entityClass = entityClass;
    }

    // Used by JiBX
    @SuppressWarnings("unused")
    private void addFieldModel(FieldModel fieldModel) {
        if (this.fieldModels.containsKey(fieldModel.getName()))
            throw new IllegalArgumentException("field `" + fieldModel.getName() + "' already exists");
        this.fieldModels.put(fieldModel.getName(), fieldModel);
    }

    // Used by JiBX
    @SuppressWarnings("unused")
    private Iterator<FieldModel> iterateFieldModels() {
        return this.fieldModels.values().iterator();
    }

    // Used by JiBX
    @SuppressWarnings("unused")
    private void addIndexModel(IndexModel indexModel) {
        if (this.indexModels.containsKey(indexModel.getName()))
            throw new IllegalArgumentException("index `" + indexModel.getName() + "' already exists");
        this.indexModels.put(indexModel.getName(), indexModel);
    }

    // Used by JiBX
    @SuppressWarnings("unused")
    private Iterator<IndexModel> iterateIndexModels() {
        return this.indexModels.values().iterator();
    }

    /**
     * Print a description of this entity.
     */
    public void print(PrintStream ps) {
        ps.print("  ");
        if (this.isAbstract)
            ps.print("abstract ");
        ps.print("entity \"" + getName() + "\"");
        if (this.parentModel != null)
            ps.print(" parent=\"" + this.parentModel.getName() + "\"");
        ps.println(" {");
        for (FieldModel fieldModel : this.fieldModels.values())
            ps.println("    " + fieldModel);
        for (IndexModel indexModel : this.indexModels.values())
            ps.println("    " + indexModel);
        ps.println("  }");
    }

    /**
     * Validate this entity. In particular, any {@link EntityModel}s that are referenced
     * by this entity must appear in the schema as well.
     *
     * <p>
     * This method should recurse on all fields.
     *
     * @param schema the schema being validated
     * @throws IllegalArgumentException if this type is invalid
     */
    public void validate(final SchemaModel schema) {

        // Check name
        Sidekar.checkValidEntityName(getName());

        // Verify I'm not my own supertype
        for (EntityModel model = this.parentModel; model != null; model = model.parentModel) {
            if (model == this)
                throw new IllegalArgumentException("entity cannot be its own parent: " + this);
        }

        // Verify my field names are valid and unique
        HashSet<String> fieldNames = new HashSet<String>();
        for (EntityModel model = this; model != null; model = model.parentModel) {
            for (FieldModel fieldModel : model.fieldModels.values()) {
                Sidekar.checkValidFieldName(fieldModel.getName());
                if (!fieldNames.add(fieldModel.getName()))
                    throw new IllegalArgumentException("duplicate field \"" + fieldModel.getName() + "\"");
            }
        }

        // Validate fields
        for (FieldModel fieldModel : this.fieldModels.values()) {
            try {
                fieldModel.validate(schema);
            } catch (IllegalArgumentException e) {
                throw new IllegalArgumentException("invalid field \"" + fieldModel.getName() + "\": " + e.getMessage(), e);
            }
        }

        // Validate indexes
        HashSet<String> indexNames = new HashSet<String>();
        for (IndexModel indexModel : this.indexModels.values()) {
            String name = indexModel.getName();
            Sidekar.checkValidIndexName(indexModel.getName());
            if (!indexNames.add(name))
                throw new IllegalArgumentException("duplicate index \"" + name + "\"");
            try {
                indexModel.validate(this);
            } catch (IllegalArgumentException e) {
                throw new IllegalArgumentException("invalid index \"" + name + "\": " + e.getMessage(), e);
            }
        }
    }

    /**
     * Determine if the given entity has the same structure as this entity.
     * In other words, a schema update should be required if and only if this returns {@code false}.
     *
     * @throws SchemaMismatchException if types are different
     */
    public void checkSameEntity(EntityModel that) {

        // Track comparisions to avoid recursion
        if (CheckPair.contains(this, that))
            return;
        CheckPair.add(this, that);

        // Check basic properties
        Sidekar.checkValidEntityName(this.getName());
        Sidekar.checkValidEntityName(that.getName());
        if (!this.getName().equals(that.getName()))
            throw new SchemaMismatchException("different names: " + that.getName() + " vs. " + this.getName());
        if (this.isAbstract != that.isAbstract)
            throw new SchemaMismatchException("different abstractness: " + that.isAbstract + " vs. " + this.isAbstract);
        EntityModel thisParent = this.parentModel;
        EntityModel thatParent = that.parentModel;
        if ((thisParent == null) != (thatParent == null))
            throw new SchemaMismatchException("parent entities differ: " + thatParent + " vs. " + thisParent);
        if (thisParent != null) {
            try {
                this.parentModel.checkSameEntity(that.parentModel);
            } catch (SchemaMismatchException e) {
                throw new SchemaMismatchException("parent entities differ: " + e.getMessage(), e);
            }
        }

        // Get fields
        HashMap<String, FieldModel> thisFieldMap = new HashMap<String, FieldModel>();
        for (FieldModel fieldModel : this.fieldModels.values()) {
            Sidekar.checkValidFieldName(fieldModel.getName());
            thisFieldMap.put(fieldModel.getName(), fieldModel);
        }
        HashMap<String, FieldModel> thatFieldMap = new HashMap<String, FieldModel>();
        for (FieldModel fieldModel : that.fieldModels.values()) {
            Sidekar.checkValidFieldName(fieldModel.getName());
            thatFieldMap.put(fieldModel.getName(), fieldModel);
        }

        // Map fields by name
        for (String name : thisFieldMap.keySet()) {
            if (!thatFieldMap.containsKey(name))
                throw new SchemaMismatchException("field \"" + name + "\" is missing");
        }
        for (String name : thatFieldMap.keySet()) {
            if (!thisFieldMap.containsKey(name))
                throw new SchemaMismatchException("field \"" + name + "\" is missing");
        }

        // Compare corresponding fields having the same name
        for (Map.Entry<String, FieldModel> entry : thisFieldMap.entrySet()) {
            String name = entry.getKey();
            FieldModel thisField = entry.getValue();
            FieldModel thatField = thatFieldMap.get(name);
            try {
                thisField.checkSameField(thatField);
            } catch (SchemaMismatchException e) {
                throw new SchemaMismatchException("field \"" + name + "\" differs: " + e.getMessage(), e);
            }
        }

        // Get indexes
        HashMap<String, IndexModel> thisIndexMap = new HashMap<String, IndexModel>();
        for (IndexModel indexModel : this.indexModels.values()) {
            Sidekar.checkValidIndexName(indexModel.getName());
            thisIndexMap.put(indexModel.getName(), indexModel);
        }
        HashMap<String, IndexModel> thatIndexMap = new HashMap<String, IndexModel>();
        for (IndexModel indexModel : that.indexModels.values()) {
            Sidekar.checkValidIndexName(indexModel.getName());
            thatIndexMap.put(indexModel.getName(), indexModel);
        }

        // Map indexes by name
        for (String name : thisIndexMap.keySet()) {
            if (!thatIndexMap.containsKey(name))
                throw new SchemaMismatchException("index \"" + name + "\" is missing");
        }
        for (String name : thatIndexMap.keySet()) {
            if (!thisIndexMap.containsKey(name))
                throw new SchemaMismatchException("index \"" + name + "\" is missing");
        }

        // Compare corresponding indexes having the same name
        for (Map.Entry<String, IndexModel> entry : thisIndexMap.entrySet()) {
            String name = entry.getKey();
            IndexModel thisIndex = entry.getValue();
            IndexModel thatIndex = thatIndexMap.get(name);
            try {
                thisIndex.checkSameIndex(thatIndex);
            } catch (SchemaMismatchException e) {
                throw new SchemaMismatchException("index \"" + name + "\" differs: " + e.getMessage(), e);
            }
        }

        // Track comparisions to avoid recursion
        CheckPair.remove(this, that);
    }

    @Override
    public String toString() {
        return "entity `" + this.getName() + "'";
    }
}

